#https://machinelearningmastery.com/simple-genetic-algorithm-from-scratch-in-python/
#genetic algorithm is a stochastic global optimazation algorithm
#evlutionary
#selection with a binary represetation and simple operators based on genetic recombination and genetic mutations
#EX:
#one max
#for continuous funciton optimization
#bitstrings
'''
parent1 = 00000
parent2 = 11111

child1 = 00011
child2 = 11100
'''
import numpy as np
#fitness, that we will minimize
def objective(candidate):
    score  = 0
    weight = 1
    for i in candidate:
        score += i* 2**weight
    return score
#selection
def selection(pop, scores, k = 3):
    #first random selection
    selection_ix = np.random.randint(len(pop)) # select a candidate from population
    for ix in np.random.randint(0, len(pop), k-1):
        #check if better
        if scores[ix] < scores[selection_ix]:
            selection_ix = ix
    return pop[selection_ix]
#crossover two parents to create two children
def crossover(p1, p2, r_cross):
    c1, c2 = p1.copy(), p2.copy()
    if np.random.rand() < r_cross:
        pt = np.random.randint(1, len(p1) - 2)
        # perform crossover
        c1 = p1[:pt] + p2[pt:]
        c2 = p2[:pt] + p1[pt:]
    return [c1,c2]


#mutation
def mutation(bitstring, r_mut):
    for i in range(len(bitstring)):
        if np.random.rand() < r_mut:
            bitstring[i] = 1 - bitstring[i]
def gentic_algorithm(objective, n_bits, n_iter, n_pop, r_cross, r_mut):
    #initial population of random bistring
    pop = [[np.random.randint(0,2) for j in range(n_bits)] for i in range(n_pop)]
    #keep track of best solution
    best, best_eval = 0, objective(pop[0])
    #enumerate generations
    for gen in range(n_iter):
        #evaluate all condidates in the population
        scores = [objective(c) for c in pop]
        #check for new best solution
        for i in range(n_pop):
            if scores[i] < best_eval:
                best, best_eval = pop[i], scores[i]
                print(">",gen,"new best f",pop[i], "=",scores[i])
        #select parents
        selected = [selection(pop, scores) for i in range(n_pop)]
        #create the next generation
        children = list()
        for i in range(0,n_pop-1, 2):
            #get selected parents in pairs
            p1, p2 = selected[i], selected[i+1]
            #crossover and mutation
            for c in crossover(p1, p2, r_cross):
                #mutation
                mutation(c, r_mut)
                #store for next generation
                children.append(c)
        #replace population
        pop = children
    return [best, best_eval]

gentic_algorithm(objective, 20,100,100, 0.3, 0.1)


'''
n_pop = 3
n_bits = 10
n_iter = 10
r_mut = 0.1 # rate of mutation
r_cross = 0.3# rate of crossover
#step 1: initial population of random bitstring
pop = ([ [np.random.randint(0,2) for _ in range(n_bits)]  for i in range(n_pop)])

print("population: ",pop)
#step 2: enumerate 
for gen in range(n_iter):
    scores = [objective(c) for c in pop]
    print("scores: ",scores)
    selected = [selection(pop, scores) for i in range(n_pop)]
    children = list()
    for i in range(0, n_pop-1,2):
        #getselected parents in pairs
        p1,p2 = selected[i], selected[i+1]
        print("parents",p1,p2)
        for c in crossover(p1,p2,r_cross):
            #mutation
            mutation(c,r_mut)
            #store for next genreation
            children.append(c)

'''
